''' <summary>
''' Base class for all classes to read data
''' </summary>
Public MustInherit Class clsData
    Implements IDisposable

    ''' <summary>
    ''' Is raised during long operations (e.g., import/export) so caller can report using UI
    ''' </summary>
    ''' <param name="Value">Current value (must alway initialize with 0 and finalize with Value=MaxValue)</param>
    ''' <param name="MaxValue">Maximum value</param>
    ''' <param name="ProgressText">Text to display</param>
    Public Event Progress(ByVal Value As Integer, ByVal MaxValue As Integer, ByVal ProgressText As String)

    Protected Sub RaiseProgress(ByVal Value As Integer, ByVal MaxValue As Integer, ByVal ProgressText As String)
        RaiseEvent Progress(Value, MaxValue, ProgressText)
    End Sub

    Public Enum enumDataType
        BMD
        DSS
        'Excel
        OleDB
        Paradox
        StoretXML
        StoretStaXML
        Text
        WDM
        LspcOut
        LspcAir
        EFDC
        CuashiSta
        YSI
    End Enum

    Protected mFilename As String
    Protected mRecordNum As Integer 'used for error reporting
    Protected mHasHeaders As Boolean
    Protected mFieldNames As Generic.List(Of String)
    Protected mItems As Generic.List(Of String)
    Protected mTableNames As Generic.List(Of String)
    Protected mStationIDs As Generic.List(Of String)
    Protected mPCodes As Generic.List(Of String)
    Protected mActiveTable As String
    Protected mActiveStationID As String
    Protected mActiveStationIDSet As Boolean
    Protected mDataType As enumDataType
    Protected mMinDate, mMaxDate As Date
    Protected mMinValue(), mMaxValue() As Single
    Protected dictFieldIndex As Generic.Dictionary(Of String, Integer)
    Protected dictPCodeIndex As Generic.Dictionary(Of String, Integer)

    Private WRDBFields() As String = {"Station_ID", "PCode", "Date_Time", "Result", "RCode", "CCode", "Agency", "LEW_Pct", "Depth_M"}

    Private WRDBFieldsUpper() As String = {"STATION_ID", "PCODE", "DATE_TIME", "RESULT", "RCODE", "CCODE", "AGENCY", "LEW_PCT", "DEPTH_M"}

    ''' <summary>
    ''' Instantiate the proper class based on the file extension (and contents if .OUT); initialize will have already been called
    ''' </summary>
    Shared Function Instantiate(ByVal FileName As String) As clsData
        Dim Data As clsData = Nothing
        Dim ext As String = IO.Path.GetExtension(FileName).ToUpper
        Select Case ext
            Case ".BMD" : Data = New clsDataBMD : Data.Initialize(FileName)
            Case ".DSS" : Data = New clsDataDSS : Data.Initialize(FileName)
            Case ".DB" : Data = New clsDataParadox : Data.Initialize(FileName)
            Case ".MDB", ".DBF", ".XLS", ".XLSX", ".XLSM" : Data = New clsDataOleDB : Data.Initialize(FileName)
            Case ".CSV", ".TXT" : Data = New clsDataText : Data.Initialize(FileName)
            Case ".WDM" : Data = New clsDataWDM : Data.Initialize(FileName)
            Case ".AIR" : Data = New clsDataLspcAir : Data.Initialize(FileName)
            Case ".DAT" : Data = New clsDataYSI : Data.Initialize(FileName)
            Case ".OUT"
                Dim sr As New IO.StreamReader(FileName)
                Dim s As String = sr.ReadLine
                sr.Close()
                If s.StartsWith("TT") Then
                    Data = New WRDB.DataSource.clsDataLspcOut : Data.Initialize(FileName)
                Else
                    Application.UseWaitCursor = False
                    s = InputBox("Enter year associated with the beginning of the simulation from which julian days are derived:", "EFDC Base Year", Now.Year)
                    If Not IsNumeric(s) Then
                        Return Nothing
                    Else
                        Data = New WRDB.DataSource.clsDataEFDC : Data.Initialize(FileName)
                        CType(Data, WRDB.DataSource.clsDataEFDC).CalendarYear = s
                    End If
                End If
        End Select
        Return Data
    End Function

    ''' <summary>
    ''' Initialize class
    ''' </summary>
    ''' <param name="_Filename">Name of file to read</param>
    Public Overridable Sub Initialize(ByVal _Filename As String)
        mFilename = _Filename
        mRecordNum = -1
        mFieldNames = New Generic.List(Of String)
        mItems = New Generic.List(Of String)
        mTableNames = New Generic.List(Of String)
        mStationIDs = New Generic.List(Of String)
        mPCodes = New Generic.List(Of String)
        mMinDate = Date.MinValue
        mMaxDate = Date.MaxValue
    End Sub

    ''' <summary>
    ''' Read next line from file into memory, storing into mItems array
    ''' Also save mLastLineRead for error reporting
    ''' </summary>
    ''' <returns>True if successful, false if eof reached and no more data or user cancelled</returns>
    Public Overridable Function Read() As Boolean
        mRecordNum += 1
        Return True
    End Function

    ''' <summary>
    ''' Returns number or records read so far
    ''' </summary>
    Public ReadOnly Property NumRead() As Integer
        Get
            Return mRecordNum + 1
        End Get
    End Property

    ''' <summary>
    ''' Returns last number of items read
    ''' </summary>
    Public ReadOnly Property NumItems() As Integer
        Get
            Return mItems.Count
        End Get
    End Property

    ''' <summary>
    ''' Resets datasource so new read operation starts at beginning of file
    ''' For array-based data sources, no override needed; others need to close then reopen
    ''' </summary>
    Public Overridable Sub Rewind()
        mRecordNum = -1
    End Sub

    ''' <summary>
    ''' Resets datasource so new read operation starts at beginning of file
    ''' For array-based data sources, no override needed; others need to close then reopen
    ''' </summary>
    Public Overridable Sub Rewind(ByVal _HasHeaders As Boolean)
        mHasHeaders = _HasHeaders
        Rewind()
    End Sub

    ''' <summary>
    ''' Return the file name of database or table
    ''' </summary>
    ''' <returns></returns>
    Public ReadOnly Property FileName() As String
        Get
            Return mFilename
        End Get
    End Property

    ''' <summary>
    ''' For data sources with multiple tables (or tabs or data subsets), return the list of table names
    ''' </summary>
    ''' <returns></returns>
    Public Overridable Function TableNames() As Generic.List(Of String)
        Return mTableNames
    End Function

    ''' <summary>
    ''' Return the list of unique station ids
    ''' </summary>
    ''' <returns></returns>
    Public Overridable Function StationIDs() As Generic.List(Of String)
        Return mStationIDs
    End Function

    ''' <summary>
    ''' Return the list of unique pcodes
    ''' </summary>
    ''' <returns></returns>
    Public Overridable Function PCodes() As Generic.List(Of String)
        Return mPCodes
    End Function

    ''' <summary>
    ''' Returns list of column headers or field names
    ''' </summary>
    Public ReadOnly Property FieldNames() As Generic.List(Of String)
        Get
            Return mFieldNames
        End Get
    End Property

    ''' <summary>
    ''' Given a fieldname, return the parsed value representing the PCode (used primarily of LSPC/EFCD import)
    ''' </summary>
    ''' <param name="FieldName">Field name</param>
    Public Overridable Function PCodeFromField(ByVal FieldName As String) As String
        Return Strings.Left(FieldName, 10).Trim
    End Function

    ''' <summary>
    ''' See if field name is found (case insensitive)
    ''' </summary>
    ''' <param name="FieldName">Name of field to check</param>
    ''' <returns>True if found</returns>
    Public Function ContainsFieldName(ByVal FieldName As String) As Boolean
        Return FieldIndex(FieldName) <> -1
    End Function

    ''' <summary>
    ''' Determine index of named field or PCode (case insensitive)
    ''' </summary>
    ''' <param name="FieldName">Name of field to check</param>
    ''' <returns>Index if found, -1 if not found</returns>
    Public Overridable Function FieldIndex(ByVal FieldName As String) As Integer
        If dictFieldIndex Is Nothing Then 'must be initialized to speed later searches
            dictFieldIndex = New Generic.Dictionary(Of String, Integer)
            For i As Integer = 0 To mFieldNames.Count - 1
                dictFieldIndex.Add(mFieldNames(i).ToUpper, i)
            Next
            dictPCodeIndex = New Generic.Dictionary(Of String, Integer)
            For i As Integer = 0 To mFieldNames.Count - 1
                Dim PCode As String = PCodeFromField(mFieldNames(i)).ToUpper
                If Not dictPCodeIndex.ContainsKey(PCode) Then
                    dictPCodeIndex.Add(PCode, i)
                End If
            Next
        End If

        Dim index As Integer = -1
        If dictFieldIndex.TryGetValue(FieldName.ToUpper, index) Then Return index
        If dictPCodeIndex.TryGetValue(FieldName.ToUpper, index) Then Return index
        Return -1
    End Function

    '''' <summary>
    '''' Retrieve specified item on last line read
    '''' </summary>
    '''' <returns>Contents of the field for last record read</returns>
    'Public ReadOnly Property Items() As Generic.List(Of String)
    '    Get
    '        Return mItems
    '    End Get
    'End Property

    ''' <summary>
    ''' Retrieve specified item on last line read
    ''' </summary>
    ''' <returns>Contents of the field for last record read</returns>
    Public ReadOnly Property Items(ByVal Index As Integer) As String
        Get
            If Index < 0 Or Index > mItems.Count - 1 Then Return "" Else Return mItems(Index)
        End Get
    End Property

    ''' <summary>
    ''' Retrieve specified item on last line read
    ''' </summary>
    ''' <returns>Contents of the field for last record read</returns>
    Public ReadOnly Property Items(ByVal FieldName As String) As String
        Get
            Dim i As Integer = FieldIndex(FieldName)
            If i = -1 Then
                Throw New ArgumentException("Field name " & FieldName & " not found in dataset.")
            Else
                Return mItems(i)
            End If
        End Get
    End Property

    ''' <summary>
    ''' Currently active table or data subset
    ''' </summary>
    Public Overridable Property ActiveTable() As String
        Get
            Return mActiveTable
        End Get
        Set(ByVal value As String)
            mRecordNum = -1
            mActiveTable = value
        End Set
    End Property

    ''' <summary>
    ''' Currently active station ID (may allow some sources to avoid long sequential reads)
    ''' </summary>
    Public Overridable Property ActiveStationID() As String
        Get
            Return mActiveStationID
        End Get
        Set(ByVal value As String)
            Rewind()
            mActiveStationIDSet = True
            mActiveStationID = value
        End Set
    End Property

    ' ''' <summary>
    ' ''' Get data table containing time series data for a specified station id and pcode
    ' ''' Table will contain date-time field and result field
    ' ''' </summary>
    'Public Overridable Function GetDataTable(ByVal StationID As String, ByVal PCode As String) As DataTable
    '    Dim dt As New DataTable
    '    With dt.Columns
    '        .Add("Date_Time", GetType(Date))
    '        .Add("Result", GetType(Single))
    '    End With
    '    Return dt
    'End Function

    ' ''' <summary>
    ' ''' Get data table containing time series data for all station ids and pcode
    ' ''' Table will contain station field, date-time field and result field
    ' ''' </summary>
    'Public Overridable Function GetDataTable(ByVal StationIDs() As String, ByVal PCode As String) As DataTable
    '    Dim dt As New DataTable
    '    With dt.Columns
    '        .Add("Station_ID", GetType(String))
    '        .Add("Date_Time", GetType(Date))
    '        .Add("Result", GetType(Single))
    '    End With
    '    Return dt
    'End Function

    ' ''' <summary>
    ' ''' Get data table containing all data in given time range for all station ids and pcode
    ' ''' Table will contain station and result field
    ' ''' </summary>
    'Public Overridable Function GetDataTable(ByVal StationIDs() As String, ByVal PCode As String, MinDate As Date, MaxDate As Date) As DataTable
    '    Dim dt As New DataTable
    '    With dt.Columns
    '        .Add("Station_ID", GetType(String))
    '        .Add("Result", GetType(Single))
    '    End With
    '    Return dt
    'End Function

    Protected Enum enumDataTableColumns
        All
        Station_Result
        Station_Date_Result
        Date_Result
    End Enum

    ''' <summary>
    ''' Get data table containing time series data for all stations and pcodes
    ''' Table will contain all source data fields
    ''' </summary>
    Protected Function SetupDataTable(DataTableColumns As enumDataTableColumns) As DataTable
        Dim dt As New DataTable
        With dt.Columns
            Select Case DataTableColumns
                Case enumDataTableColumns.All
                    For Each s As String In FieldNames
                        .Add(s, GetType(String))
                    Next
                Case enumDataTableColumns.Station_Result
                    .Add("Station_ID", GetType(String))
                    .Add("Result", GetType(Single))
                Case enumDataTableColumns.Station_Date_Result
                    .Add("Station_ID", GetType(String))
                    .Add("Date_Time", GetType(Date))
                    .Add("Result", GetType(Single))
                Case enumDataTableColumns.Date_Result
                    .Add("Date_Time", GetType(Date))
                    .Add("Result", GetType(Single))
            End Select
        End With
        Return dt
    End Function

    ''' <summary>
    ''' Get data table containing time series data for all records
    ''' </summary>
    Public Function GetDataTable() As DataTable
        Dim dt As DataTable = SetupDataTable(enumDataTableColumns.All)
        While Read()
            Dim dr As DataRow = dt.NewRow
            For i As Integer = 0 To NumItems - 1
                dr.Item(i) = Items(i)
            Next
            dt.Rows.Add(dr)
        End While
        Return dt
    End Function

    ''' <summary>
    ''' Get data table containing time series data for a specified station id and pcode
    ''' Table will contain date-time field and result field
    ''' </summary>
    Public Overridable Function GetDataTable(ByVal StationID As String, ByVal PCode As String) As DataTable
        Dim itemNum As Integer = -1
        If mPCodes.Contains(PCode) Then
            itemNum = mPCodes.IndexOf(PCode)
        Else
            Throw New ApplicationException("File does not contain the column: " & PCode)
            Return Nothing
        End If
        ActiveStationID = StationID
        Dim dt As DataTable = SetupDataTable(enumDataTableColumns.Date_Result)
        While Read()
            dt.Rows.Add(Items("Date_Time"), Items(itemNum))
        End While
        Return dt
    End Function

    ''' <summary>
    ''' Get data table containing time series data for a specified station id 
    ''' Table will contain all fields
    ''' </summary>
    Public Function GetDataTable(ByVal StationID As String) As DataTable
        Dim dt As DataTable = SetupDataTable(enumDataTableColumns.All)
        ActiveStationID = StationID
        While Read()
            Dim row As DataRow = dt.NewRow
            For i As Integer = 0 To NumItems - 1
                row(i) = Items(i)
            Next
            dt.Rows.Add(row)
        End While
        Return dt
    End Function

    ''' <summary>
    ''' Get data table containing all data in given time range for all station ids and pcode
    ''' Table will contain station and result field
    ''' </summary>
    ''' <remarks>
    ''' Called by GIS for for animation; this routine does brute-force loading; suggest override as follows:
    ''' <list>
    ''' BMD: direct access to dates and times
    ''' LSPC: no more efficient means to do this (WRDB 5.1 completely changes how animation is done)
    ''' SQL: perform SQL query
    ''' </list>
    ''' </remarks>
    Public Overridable Function GetDataTable(ByVal StationIDs() As String, ByVal PCode As String, MinDate As Date, MaxDate As Date) As DataTable
        Dim dt As DataTable = SetupDataTable(enumDataTableColumns.Station_Result)
        For Each StaID As String In StationIDs
            ActiveStationID = StaID
            While Read()
                Dim DateTime As Date = Items("Date_Time")
                If DateTime >= MinDate And DateTime <= MaxDate Then dt.Rows.Add(StaID, Items(PCode))
            End While
        Next
        Return dt
    End Function

    Public Overridable ReadOnly Property MinDate() As Date
        Get
            If mMinDate = Date.MinValue Then GetMinMaxDates()
            Return mMinDate
        End Get
    End Property

    Public Overridable ReadOnly Property MaxDate() As Date
        Get
            If mMaxDate = Date.MaxValue Then GetMinMaxDates()
            Return mMaxDate
        End Get
    End Property

    Public Overridable ReadOnly Property MinValue(PCode As String) As Single
        Get
            If mMinValue Is Nothing Then GetMinMaxValues()
            Return mMinValue(mPCodes.IndexOf(PCode))
        End Get
    End Property

    Public Overridable ReadOnly Property MaxValue(PCode As String) As Single
        Get
            If mMaxValue Is Nothing Then GetMinMaxValues()
            Return mMaxValue(mPCodes.IndexOf(PCode))
        End Get
    End Property

    ''' <summary>
    ''' Determine min/max values for each pcodes regardless of station id; generic version uses brute force by looping over all entries
    ''' </summary>
    ''' <remarks></remarks>
    Protected Overridable Sub GetMinMaxValues()
        ReDim mMinValue(mPCodes.Count - 1)
        ReDim mMaxValue(mPCodes.Count - 1)
        For v As Integer = 0 To mPCodes.Count - 1
            mMinValue(v) = Double.MaxValue
            mMaxValue(v) = Double.MinValue
        Next
        Rewind()
        While Read()
            For v As Integer = 0 To mPCodes.Count - 1
                Dim value As Double = Items(mPCodes(v))
                mMinValue(v) = Math.Min(mMinValue(v), value)
                mMaxValue(v) = Math.Max(mMaxValue(v), value)
            Next
        End While
        Rewind()
    End Sub

    ''' <summary>
    ''' Determine min/max values for each pcodes regardless of station id; generic version uses brute force by looping over all entries
    ''' </summary>
    ''' <remarks></remarks>
    Protected Overridable Sub GetMinMaxDates()
        mMinDate = Date.MaxValue
        mMaxDate = Date.MinValue
        Dim datefld As Integer = FieldIndex("Date_Time")
        If datefld = -1 Then Exit Sub
        Rewind()
        While Read()
            Dim dt As Date = mItems(datefld)
            If dt < mMinDate Then mMinDate = dt
            If dt > mMaxDate Then mMaxDate = dt
        End While
        Rewind()
    End Sub

    ''' <summary>
    ''' Convenient method of determining data file type
    ''' </summary>
    Public ReadOnly Property DataType() As enumDataType
        Get
            Return mDataType
        End Get
    End Property

    ''' <summary>
    ''' Determine percent of file that has been read
    ''' </summary>
    ''' <returns>Percent of file read</returns>
    Public MustOverride Function PercentComplete() As Double

    Protected Function RemoveBrackets(ByVal s As String) As String
        Return s.Replace("[", "").Replace("]", "").Replace("'", "").Replace("$", "")
    End Function

    ''' <summary>
    ''' Close the data source; override this to close any open files
    ''' </summary>
    Public Overridable Sub Close()
        mFieldNames.Clear()
        mItems.Clear()
        mTableNames.Clear()
        mStationIDs.Clear()
        mPCodes.Clear()
    End Sub

    Public Overloads Sub Dispose() Implements IDisposable.Dispose
        Dispose(True)
        GC.SuppressFinalize(Me)
    End Sub

    Private disposed As Boolean = False

    Protected Overridable Overloads Sub Dispose(ByVal disposing As Boolean)
        If Not Me.disposed Then
            If disposing Then
                'put disposal items here
            End If
        End If
        Me.disposed = True
    End Sub

    Protected Overrides Sub Finalize()
        Dispose(False)
        MyBase.Finalize()
    End Sub

    ''' <summary>
    ''' Round date-time value to the nearest minute
    ''' </summary>
    ''' <param name="Original">Value to be rounded</param>
    ''' <returns></returns>
    ''' <remarks>This is necessary because slight round-off errors when truncated will appear as, e.g., 11:59 rather than 12:00</remarks>
    Shared Function RoundDateTime(ByVal Original As Date) As Date
        With Original
            If .Second >= 30 Then Original = .AddMinutes(1)
            Return New Date(.Year, .Month, .Day, .Hour, .Minute, 0)
        End With
    End Function

    Public Function IsWRDBField(ByVal FieldName As String) As Boolean
        Return Array.IndexOf(WRDBFieldsUpper, FieldName.ToUpper) <> -1
        'For Each s As String In WRDBFields
        '    If FieldName.Equals(s, StringComparison.OrdinalIgnoreCase) Then Return True
        'Next
        'Return False
    End Function

End Class

